import glob
import os
import stat
import tarfile
from collections import OrderedDict
from pathlib import Path
from shutil import copyfile, copytree, rmtree

import numpy as np
import yaml

from hls4ml.backends import get_backend
from hls4ml.model.layers import Conv1D, Conv2D, Conv2DBatchnorm, Dense
from hls4ml.utils.fixed_point_utils import FixedPointEmulator, ceil_log2, uint_to_binary
from hls4ml.writer.writers import Writer

config_filename = 'hls4ml_config.yml'


class QuartusWriter(Writer):
    def next_pow2(self, x):
        return 1 << (x - 1).bit_length()

    def __make_dat_file(self, original_path, project_path):
        """
        Convert other input/output data types into a dat file, which is
        a text file with the falttened matrix printed out. Note that ' ' is
        assumed to be the delimiter.
        """

        # Take in data from current supported data files
        if original_path[-3:] == "npy":
            data = np.load(original_path)
        else:
            raise Exception("Unsupported input/output data files.")

        # Faltten data, just keep first dimension
        data = data.reshape(data.shape[0], -1)

        def print_data(f):
            for i in range(data.shape[0]):
                for j in range(data.shape[1]):
                    f.write(str(data[i][j]) + " ")
                f.write("\n")

        # Print out in dat file
        with open(project_path, "w") as f:
            print_data(f)

    def get_max_reuse_factor(self, model):
        max_rf = 0
        for layer in model.get_layers():
            rf = int(layer.get_attr('reuse_factor'))
            if rf > max_rf:
                max_rf = rf
        return max_rf

    def print_array_to_cpp(self, var, layer, odir):
        """Write a weights array to C++ header files.

        Args:
            var (WeightVariable): Weight to write
            layer (Layer): Instance of the layer to which the weights belong
            odir (str): Output directory
        """
        h_file = open(f"{odir}/firmware/weights/{var.name}.h", "w")

        # meta data
        h_file.write(f"//Numpy array shape {var.shape}\n")
        h_file.write(f"//Min {np.min(var.min):.12f}\n")
        h_file.write(f"//Max {np.max(var.max):.12f}\n")
        h_file.write(f"//Number of zeros {var.nzeros}\n")
        h_file.write("\n")

        h_file.write(f"#ifndef {var.name.upper()}_H_\n")
        h_file.write(f"#define {var.name.upper()}_H_\n")
        h_file.write("\n")

        rf = int(layer.get_attr('reuse_factor', 1))
        weight_header = '#ifdef __INTELFPGA_COMPILER__\n'

        weight_size = 0
        if isinstance(layer, (Conv2D, Conv2DBatchnorm)):
            weight_size = (
                layer.get_attr('impl_filt_height')
                * layer.get_attr('impl_filt_width')
                * layer.get_attr('n_filt')
                * layer.get_attr('n_chan')
            )
        elif isinstance(layer, (Conv1D)):
            weight_size = layer.get_attr('impl_filt_width') * layer.get_attr('n_filt') * layer.get_attr('n_chan')
        elif isinstance(layer, (Dense)):
            weight_size = layer.get_attr('n_in') * layer.get_attr('n_out')

        if rf == 1 or var.name[0] == 'b' or weight_size <= 2048 or (var.name[0] == 'w' and var.type.precision.width < 3):
            weight_header += 'hls_init_on_powerup\n'
        else:
            block_factor = (layer.get_attr('n_in') * layer.get_attr('n_out')) / rf
            nbanks = int(2 ** np.ceil(np.log2(block_factor)) / 2)
            var_width = int(np.ceil(var.type.precision.width / 8))
            bwidth = self.next_pow2(var_width)
            weight_header += (
                f'hls_bankwidth({bwidth})\nhls_numbanks({nbanks})\nhls_max_replicates(1)\nhls_memory_impl("BLOCK_RAM")\n'
            )
        weight_header += '#endif\n'
        if var.storage.lower() == 'bram':
            weight_header += 'static '
        else:
            weight_header += 'static const '
        h_file.write(weight_header + var.definition_cpp() + " = {")

        # fill c++ array.
        # not including internal brackets for multidimensional case
        sep = ''
        for x in var:
            h_file.write(sep + x)
            sep = ", "
        h_file.write("};\n")
        h_file.write("\n#endif\n")
        h_file.close()

    def write_project_dir(self, model):
        """Write the base project directory

        Args:
            model (ModelGraph): the hls4ml model.
        """
        if not os.path.isdir(f"{model.config.get_output_dir()}/firmware/weights"):
            os.makedirs(f"{model.config.get_output_dir()}/firmware/weights")

    def write_project_cpp(self, model):
        """Write the main architecture source file (myproject.cpp)

        Args:
            model (ModelGraph): the hls4ml model.
        """
        project_name = model.config.get_project_name()

        filedir = os.path.dirname(os.path.abspath(__file__))
        f = open(os.path.join(filedir, '../templates/quartus/firmware/myproject.cpp'))
        fout = open(f'{model.config.get_output_dir()}/firmware/{project_name}.cpp', 'w')

        model_inputs = model.get_input_variables()
        model_outputs = model.get_output_variables()
        model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram']

        io_type = model.config.get_config_value('IOType')
        indent = '   '
        brams_str = ', \n'.join([indent + b.definition_cpp(as_reference=False) for b in model_brams])

        for line in f.readlines():
            # Add headers to weights and biases
            if 'myproject' in line:
                newline = line.replace('myproject', project_name)

            # Intel HLS 'streams' need to be passed by reference to top-level entity or declared as global variables
            # Streams cannot be declared inside a function
            # Therefore, layer connections (inputs/outputs) are declared here
            elif '// hls-fpga-machine-learning insert inter-task streams' in line:
                newline = line
                if io_type == 'io_stream':
                    for layer in model.get_layers():
                        vars = layer.get_variables()
                        for var in vars:
                            def_cpp = var.definition_cpp()
                            if def_cpp is not None:
                                newline += def_cpp + ';\n'

            # Instantiate GCC top-level function, to be used during GCC compilation / hls4ml.predict()
            elif '// hls-fpga-machine-learning instantiate GCC top-level' in line:
                newline = line
                if io_type == 'io_stream':
                    newline += f'void {project_name}(\n'
                    for inp in model_inputs:
                        newline += indent + f'stream_in<{inp.type.name}> &{inp.name}_stream,\n'
                    for out in model_outputs:
                        newline += indent + f'stream_out<{out.type.name}> &{out.name}_stream,\n'
                    newline = newline[:-2]  # Remove the tailing ',\n'
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n) {\n'
                if io_type == 'io_parallel':
                    newline = f'output_data {project_name}(\n'
                    newline += indent + 'input_data inputs'
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n) {\n'

            # Instantiate HLS top-level function, to be used during HLS synthesis
            elif '// hls-fpga-machine-learning instantiate HLS top-level' in line:
                newline = line
                if io_type == 'io_stream':
                    newline += f'component void {project_name}(\n'
                    for inp in model_inputs:
                        newline += indent + f'stream_in<{inp.type.name}> &{inp.name}_stream,\n'
                    for out in model_outputs:
                        newline += indent + f'stream_out<{out.type.name}> &{out.name}_stream,\n'
                    newline = newline[:-2]  # Remove the tailing ',\n'\
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n) {\n'
                if io_type == 'io_parallel':
                    newline += f'component output_data {project_name}(\n'
                    newline += indent + 'input_data inputs'
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n) {\n'

            # Insert HLS pragmas such as maximum frequency, initiation interval etc.
            elif '// hls-fpga-machine-learning insert cpragmas' in line:
                newline = line
                if io_type == 'io_parallel':
                    newline += 'hls_max_concurrency(0)\n'
                    newline += f'hls_component_ii({self.get_max_reuse_factor(model)})\n'
                clock_mhz = 1000 / (model.config.get_config_value('ClockPeriod'))
                newline += f'hls_scheduler_target_fmax_mhz({np.ceil(clock_mhz).astype(int)})\n'

            # In io_parallel, an output (struct) is returned from the top-level function
            # Therefore, it needs to be initialised before returning
            # In io_stream, the input is of type 'stream_in' and output is of type 'stream_out'
            # However, individual layers accept the type 'stream'
            # Therefore, data is first read from 'stream_in', written to 'stream' and propagated through network
            elif '// hls-fpga-machine-learning initialize input/output' in line:
                if io_type == 'io_stream':
                    newline = line
                    for inp in model_inputs:
                        newline += indent + f'for (size_t i = 0; i < {inp.size_cpp()} / {inp.type.name}::size; i++) {{\n'
                        newline += indent + f'  {inp.type.name} tmp = {inp.name}_stream.read();\n'
                        newline += indent + f'  {inp.name}.write(tmp);\n'
                        newline += indent + '}\n'
                else:
                    newline = line
                    newline += indent + 'hls_register output_data outputs;\n'

            # Insert weights
            elif '// hls-fpga-machine-learning insert weights' in line:
                newline = line
                for layer in model.get_layers():
                    for w in layer.get_weights():
                        if w not in model_brams:
                            newline += f'#include "weights/{w.name}.h"\n'

            # Insert test weights
            elif '// hls-fpga-machine-learning insert test weights' in line:
                newline = line
                for layer in model.get_layers():
                    for w in layer.get_weights():
                        newline += f'#include "weights/{w.name}_test.h"\n'

            # Neural net instantiation
            elif '// hls-fpga-machine-learning insert layers' in line:
                newline = line + '\n'
                model_inputs = model.get_input_variables()
                model_outputs = model.get_output_variables()
                for layer in model.get_layers():
                    if io_type != 'io_stream':
                        vars = layer.get_variables()
                        for var in vars:
                            if var not in model_inputs and var not in model_outputs:
                                def_cpp = var.definition_cpp()
                                if def_cpp is not None:
                                    newline += '    ' + def_cpp + ';\n'
                    func = layer.get_attr('function_cpp', None)
                    if func:
                        newline += '    ' + func + '\n'
                        if model.config.trace_output and layer.get_attr('trace', False):
                            newline += '#ifndef HLS_SYNTHESIS\n'
                            for var in vars:
                                newline += '    nnet::save_layer_output<{}>({}, "{}", {});\n'.format(
                                    var.type.name, var.name, layer.name, var.size_cpp()
                                )
                            newline += '#endif\n'
                        newline += '\n'

            # In io_parallel, a return is required; for more details see myproject.cpp & myproject.h
            elif '// hls-fpga-machine-learning return' in line:
                if io_type == 'io_stream':
                    newline = line
                    for out in model_outputs:
                        newline += indent + f'for (size_t i = 0; i < {out.size_cpp()} / {out.type.name}::size; i++) {{\n'
                        newline += indent + f'  {out.type.name} tmp = {out.name}.read();\n'
                        newline += indent + f'  {out.name}_stream.write(tmp);\n'
                        newline += indent + '}\n'
                    newline += '}\n'
                else:
                    newline = line
                    newline += indent + 'return outputs;\n'
                    newline += '}\n'

            # Just copy line
            else:
                newline = line

            fout.write(newline)

        f.close()
        fout.close()

    def write_project_header(self, model):
        """Write the main architecture header file (myproject.h)

        Args:
            model (ModelGraph): the hls4ml model.
        """

        project_name = model.config.get_project_name()

        filedir = os.path.dirname(os.path.abspath(__file__))
        f = open(os.path.join(filedir, '../templates/quartus/firmware/myproject.h'))
        fout = open(f'{model.config.get_output_dir()}/firmware/{project_name}.h', 'w')

        model_inputs = model.get_input_variables()
        model_outputs = model.get_output_variables()
        model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram']

        # io_parallel and io_stream instantiate the top-level function differently
        io_type = model.config.get_config_value('IOType')
        indent = '    '
        brams_str = ', \n'.join([indent + b.definition_cpp(as_reference=False) for b in model_brams])

        for line in f.readlines():
            if 'MYPROJECT' in line:
                newline = line.replace('MYPROJECT', format(project_name.upper()))

            elif 'myproject' in line:
                newline = line.replace('myproject', project_name)

            elif '// hls-fpga-machine-learning instantiate GCC top-level' in line:
                newline = line
                # For io_stream, input and output are passed by reference; see myproject.h & myproject.cpp for more details

                if io_type == 'io_stream':
                    newline += f'void {project_name}(\n'
                    for inp in model_inputs:
                        newline += indent + f'stream_in<{inp.type.name}> &{inp.name}_stream,\n'
                    for out in model_outputs:
                        newline += indent + f'stream_out<{out.type.name}> &{out.name}_stream,\n'
                    newline = newline[:-2]  # Remove the tailing ',\n'
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n);\n'
                # In io_parallel, a struct is returned; see myproject.h & myproject.cpp for more details
                else:
                    newline += f'output_data {project_name}(\n'
                    newline += indent + 'input_data inputs'
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n);\n'

            # Similar to GCC instantiation, but with the keyword 'component'
            elif '// hls-fpga-machine-learning instantiate HLS top-level' in line:
                newline = line
                if io_type == 'io_stream':
                    newline += f'component void {project_name}(\n'
                    for inp in model_inputs:
                        newline += indent + f'stream_in<{inp.type.name}> &{inp.name}_stream,\n'
                    for out in model_outputs:
                        newline += indent + f'stream_out<{out.type.name}> &{out.name}_stream,\n'
                    newline = newline[:-2]  # Remove the tailing ',\n'
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n);\n'
                else:
                    newline += f'component output_data {project_name}(\n'
                    newline += indent + 'input_data inputs'
                    if model_brams:
                        newline += ',\n' + brams_str
                    newline += '\n);\n'

            elif '// hls-fpga-machine-learning insert cpragmas' in line:
                newline = line
                if io_type == 'io_parallel':
                    newline += 'hls_max_concurrency(0)\n'
                    newline += f'hls_component_ii({self.get_max_reuse_factor(model)})\n'
                clock_mhz = 1000 / (model.config.get_config_value('ClockPeriod'))
                newline += f'hls_scheduler_target_fmax_mhz({np.ceil(clock_mhz).astype(int)})\n'

            # For io_stream, no inputs/outputs are instantiated, as they are passed by reference
            # For io_parallel, input/output structs are required
            elif '// hls-fpga-machine-learning insert inputs' in line:
                newline = line
                if io_type != 'io_stream':
                    newline += 'struct input_data { \n'
                    for inp in model_inputs:
                        newline += indent + inp.definition_cpp() + ';\n'
                    newline += '};\n'
            elif '// hls-fpga-machine-learning insert outputs' in line:
                newline = line
                if io_type != 'io_stream':
                    newline += 'struct output_data { \n'
                    for out in model_outputs:
                        newline += indent + out.definition_cpp() + ';\n'
                    newline += '};\n'
            # Simply copy line, if no inserts are required
            else:
                newline = line

            fout.write(newline)

        f.close()
        fout.close()

    def write_defines(self, model):
        """Write the C++ type definitions file (defines.h)

        Args:
            model (ModelGraph): the hls4ml model.
        """
        filedir = os.path.dirname(os.path.abspath(__file__))
        f = open(os.path.join(filedir, '../templates/quartus/firmware/defines.h'))
        fout = open(f'{model.config.get_output_dir()}/firmware/defines.h', 'w')

        for line in f.readlines():
            # Insert numbers
            if '// hls-fpga-machine-learning insert numbers' in line:
                newline = line

                defines_list = []
                for layer in model.get_layers():
                    defines = ''
                    for k, v in layer.get_output_variable().get_shape():
                        defines += f'#define {k} {v}\n'

                    defines_list.append(defines)

                newline += ''.join(defines_list)

            elif '// hls-fpga-machine-learning insert layer-precision' in line:
                newline = line
                all_precision = OrderedDict()
                for layer in model.get_layers():
                    layer_precision = layer.get_layer_precision()
                    for type_name, type_var in layer_precision.items():
                        # Ensure that layer's types doesn't override existing types
                        # This can happen in case of InplaceVariable types
                        if type_name not in all_precision:
                            all_precision[type_name] = type_var
                for used_type in all_precision.values():
                    newline += used_type.definition_cpp()

            else:
                newline = line
            fout.write(newline)
        f.close()
        fout.close()

    def write_parameters(self, model):
        """Write the C++ layer config file (parameters.h)

        Args:
            model (ModelGraph): the hls4ml model.
        """
        filedir = os.path.dirname(os.path.abspath(__file__))
        f = open(os.path.join(filedir, '../templates/quartus/firmware/parameters.h'))
        fout = open(f'{model.config.get_output_dir()}/firmware/parameters.h', 'w')

        for line in f.readlines():
            if '// hls-fpga-machine-learning insert includes' in line:
                newline = line
                for include in sorted(set(sum((layer.get_attr('include_header', []) for layer in model.get_layers()), []))):
                    newline += '#include "%s"\n' % include

            elif "// hls-fpga-machine-learning insert layer-config" in line:
                newline = line
                for layer in model.get_layers():
                    config = layer.get_attr('config_cpp', None)
                    if config:
                        newline += config + '\n'
            else:
                newline = line
            fout.write(newline)
        f.close()
        fout.close()

    def write_weights(self, model):
        """Write the weights into header files

        Args:
            model (ModelGraph): the hls4ml model.
        """
        for layer in model.get_layers():
            for weights in layer.get_weights():
                self.print_array_to_cpp(weights, layer, model.config.get_output_dir())

    def write_testbench_parallel(self, model):
        """Write the testbench file for io_parallel (myproject_test.cpp and input/output .dat files)

        Args:
            model (ModelGraph): the hls4ml model.
        """
        if len(model.get_output_variables()) != 1:
            print("WARNING:  The testbench only supports one output variable. Leaving empty testbench")
            return

        outvar = model.get_output_variables()[0]

        filedir = os.path.dirname(os.path.abspath(__file__))

        if not os.path.exists(f'{model.config.get_output_dir()}/tb_data/'):
            os.mkdir(f'{model.config.get_output_dir()}/tb_data/')

        input_data = model.config.get_config_value('InputData')
        output_predictions = model.config.get_config_value('OutputPredictions')

        if input_data:
            if input_data[-3:] == "dat":
                copyfile(input_data, f'{model.config.get_output_dir()}/tb_data/tb_input_features.dat')
            else:
                self.__make_dat_file(input_data, f'{model.config.get_output_dir()}/tb_data/tb_input_features.dat')

        if output_predictions:
            if output_predictions[-3:] == "dat":
                copyfile(output_predictions, f'{model.config.get_output_dir()}/tb_data/tb_output_predictions.dat')
            else:
                self.__make_dat_file(
                    output_predictions, f'{model.config.get_output_dir()}/tb_data/tb_output_predictions.dat'
                )

        f = open(os.path.join(filedir, '../templates/quartus/myproject_test_parallel.cpp'))
        fout = open(f'{model.config.get_output_dir()}/{model.config.get_project_name()}_test.cpp', 'w')

        model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram']

        for line in f.readlines():
            indent = ' ' * (len(line) - len(line.lstrip(' ')))

            if 'myproject' in line:
                newline = line.replace('myproject', model.config.get_project_name())
            elif '// hls-fpga-machine-learning insert bram' in line:
                newline = line
                for bram in model_brams:
                    newline += f'#include \"firmware/weights/{bram.name}.h\"\n'
            elif '// hls-fpga-machine-learning insert data' in line:
                newline = line
                newline += '      std::vector<float>::const_iterator in_begin = in.cbegin();\n'
                newline += '      std::vector<float>::const_iterator in_end;\n'
                newline += '      inputs.emplace_back();\n'
                for inp in model.get_input_variables():
                    newline += f'      in_end = in_begin + ({inp.size_cpp()});\n'
                    newline += f'      std::copy(in_begin, in_end, inputs.back().{inp.member_name});\n'
                    newline += '      in_begin = in_end;\n'
                newline += '      outputs.emplace_back();\n'
            elif '// hls-fpga-machine-learning insert zero' in line:
                newline = line
                newline += indent + 'for(int i = 0; i < num_iterations; i++) {\n'
                for inp in model.get_input_variables():
                    newline += indent + '  inputs.emplace_back();\n'
                    newline += indent + '  outputs.emplace_back();\n'
                    newline += indent + f'  std::fill_n(inputs[i].{inp.member_name}, {inp.size_cpp()}, 0.0);\n'
                newline += indent + '}\n'

            elif '// hls-fpga-machine-learning insert top-level-function' in line:
                newline = line
                newline += indent + 'for(int i = 0; i < num_iterations; i++) {\n'
                newline += indent + f'  ihc_hls_enqueue(&outputs[i], {model.config.get_project_name()}, inputs[i]'
                if model_brams:
                    bram_vars = ','.join([b.name for b in model_brams])
                    newline += f', {bram_vars});\n'
                else:
                    newline += ');\n'
                newline += indent + '}\n'
            elif 'hls-fpga-machine-learning insert run' in line:
                newline = line
                newline += '    ' + f'ihc_hls_component_run_all({model.config.get_project_name()});\n'
            elif '// hls-fpga-machine-learning insert predictions' in line:
                newline = line
                newline += indent + f'for(int i = 0; i < {outvar.size_cpp()}; i++) {{\n'
                newline += indent + '  std::cout << predictions[j][i] << " ";\n'
                newline += indent + '}\n'
                newline += indent + 'std::cout << std::endl;\n'
            elif '// hls-fpga-machine-learning insert tb-output' in line:
                newline = line
                newline += indent + f'for(int i = 0; i < {outvar.size_cpp()}; i++) {{\n'
                newline += indent + f'  fout << outputs[j].{outvar.member_name}[i] << " ";\n'
                newline += indent + '}\n'
                newline += indent + 'fout << std::endl;\n'
            elif (
                '// hls-fpga-machine-learning insert output' in line
                or '// hls-fpga-machine-learning insert quantized' in line
            ):
                newline = line
                newline += indent + f'for(int i = 0; i < {outvar.size_cpp()}; i++) {{\n'
                newline += indent + f'  std::cout << outputs[j].{outvar.member_name}[i] << " ";\n'
                newline += indent + '}\n'
                newline += indent + 'std::cout << std::endl;\n'
            else:
                newline = line

            fout.write(newline)

        f.close()
        fout.close()

    def write_testbench_stream(self, model):
        """Write the testbench file for io_stream (myproject_test.cpp and input/output .dat files)

        Args:
            model (ModelGraph): the hls4ml model.
        """
        if len(model.get_output_variables()) != 1:
            print("WARNING:  The testbench only supports one output variable. Leaving empty testbench")
            return

        outvar = model.get_output_variables()[0]

        model_inputs = model.get_input_variables()
        model_outputs = model.get_output_variables()

        filedir = os.path.dirname(os.path.abspath(__file__))

        if not os.path.exists(f'{model.config.get_output_dir()}/tb_data/'):
            os.mkdir(f'{model.config.get_output_dir()}/tb_data/')

        input_data = model.config.get_config_value('InputData')
        output_predictions = model.config.get_config_value('OutputPredictions')

        if input_data:
            if input_data[-3:] == "dat":
                copyfile(input_data, f'{model.config.get_output_dir()}/tb_data/tb_input_features.dat')
            else:
                self.__make_dat_file(input_data, f'{model.config.get_output_dir()}/tb_data/tb_input_features.dat')

        if output_predictions:
            if output_predictions[-3:] == "dat":
                copyfile(output_predictions, f'{model.config.get_output_dir()}/tb_data/tb_output_predictions.dat')
            else:
                self.__make_dat_file(
                    output_predictions, f'{model.config.get_output_dir()}/tb_data/tb_output_predictions.dat'
                )

        f = open(os.path.join(filedir, '../templates/quartus/myproject_test_stream.cpp'))
        fout = open(f'{model.config.get_output_dir()}/{model.config.get_project_name()}_test.cpp', 'w')

        model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram']

        for line in f.readlines():
            indent = ' ' * (len(line) - len(line.lstrip(' ')))

            if 'myproject' in line:
                newline = line.replace('myproject', model.config.get_project_name())

            elif '// hls-fpga-machine-learning insert bram' in line:
                newline = line
                for bram in model_brams:
                    newline += f'#include \"firmware/weights/{bram.name}.h\"\n'

            elif '// hls-fpga-machine learning instantiate inputs and outputs' in line:
                newline = line
                for inp in model_inputs:
                    newline += indent + f'stream_in<{inp.type.name}> {inp.name}_input;\n'
                for out in model_outputs:
                    newline += indent + f'stream_out<{out.type.name}> {out.name}_output;\n'

            # TODO - This is one-input specific (are multiple model inputs needed at all?)
            elif '// hls-fpga-machine-learning insert data' in line:
                newline = line
                c = 0
                for inp in model_inputs:
                    newline += indent + f'float vals_{c}[{inp.size_cpp()}]; \n'
                    newline += indent + f'for (int j = 0 ; j < {inp.size_cpp()} ; j++) {{\n'
                    newline += indent + indent + f'vals_{c}[j] = in[j]; \n'
                    newline += indent + '}\n'
                    newline += (
                        indent
                        + f'nnet::convert_data<float, {inp.type.name}, {inp.size_cpp()}>(vals_{c}, {inp.name}_input);\n'
                    )
                    c += 1

            elif '// hls-fpga-machine-learning insert zero' in line:
                newline = line
                c = 0
                for inp in model_inputs:
                    newline += indent + f'float vals_{c}[{inp.size_cpp()}]; \n'
                    newline += indent + f'for (int j = 0 ; j < {inp.size_cpp()} ; j++) {{\n'
                    newline += indent + indent + f'vals_{c}[j] = 0.0; \n'
                    newline += indent + '}\n'
                    newline += (
                        indent
                        + f'nnet::convert_data<float, {inp.type.name}, {inp.size_cpp()}>(vals_{c}, {inp.name}_input);\n'
                    )
                    c += 1

            elif '// hls-fpga-machine-learning insert top-level-function' in line:
                newline = line
                input_params = ', '.join([f'{i.name}_input' for i in model_inputs])
                output_params = ', '.join([f'{o.name}_output' for o in model_outputs])
                newline += (
                    indent + f'ihc_hls_enqueue_noret(&{model.config.get_project_name()}, {input_params}, {output_params}'
                )
                if model_brams:
                    bram_vars = ','.join([b.name for b in model_brams])
                    newline += f', {bram_vars});\n'
                else:
                    newline += ');\n'

            elif 'hls-fpga-machine-learning insert run' in line:
                newline = line
                newline += indent + f'ihc_hls_component_run_all({model.config.get_project_name()});\n'

            elif '// hls-fpga-machine-learning convert output' in line:
                newline = line
                newline += indent + f'float res[{outvar.size_cpp()}];\n'
                newline += indent + 'nnet::convert_data_back<{}, float, {}>({}_output, res);\n'.format(
                    outvar.type.name, outvar.size_cpp(), outvar.name
                )

            elif '// hls-fpga-machine-learning insert tb-output' in line:
                newline += indent + f'for(int i = 0; i < {outvar.size_cpp()}; i++) {{\n'
                newline += indent + '  fout << res[i] << " ";\n'
                newline += indent + '}\n'
                newline += indent + 'fout << std::endl;\n'

            elif '// hls-fpga-machine-learning print predictions' in line:
                newline = line
                newline += indent + f'for(int i = 0; i < {outvar.size_cpp()}; i++) {{\n'
                newline += indent + '  std::cout << predictions[iteration][i] << " ";\n'
                newline += indent + '}\n'
                newline += indent + 'std::cout << std::endl;\n'

            elif '// hls-fpga-machine-learning print output' in line:
                newline = line
                newline += indent + f'for(int i = 0; i < {outvar.size_cpp()}; i++) {{\n'
                newline += indent + '  std::cout << res[i] << " "; \n'
                newline += indent + '} \n'
                newline += indent + 'std::cout << std::endl; \n'
            else:
                newline = line

            fout.write(newline)

        f.close()
        fout.close()

    def write_test_bench(self, model):
        """Write the testbench

        Args:
            model (ModelGraph): the hls4ml model.
        """
        # TODO - This function only works with one model input
        # (NOT one data point - it works as expected with multiple data points)
        io_type = model.config.get_config_value('IOType')
        if io_type == 'io_parallel':
            self.write_testbench_parallel(model)
        elif io_type == 'io_stream':
            self.write_testbench_stream(model)

    def write_bridge(self, model):
        """Write the Python-C++ bridge (myproject_bridge.cpp)

        Args:
            model (ModelGraph): the hls4ml model.
        """

        filedir = os.path.dirname(os.path.abspath(__file__))
        f = open(os.path.join(filedir, '../templates/quartus/myproject_bridge.cpp'))
        fout = open(f'{model.config.get_output_dir()}/{model.config.get_project_name()}_bridge.cpp', 'w')

        model_inputs = model.get_input_variables()
        model_outputs = model.get_output_variables()
        model_brams = [var for var in model.get_weight_variables() if var.storage.lower() == 'bram']

        io_type = model.config.get_config_value('IOType')
        indent = '    '

        for line in f.readlines():
            if 'MYPROJECT' in line:
                newline = line.replace('MYPROJECT', format(model.config.get_project_name().upper()))

            elif 'myproject' in line:
                newline = line.replace('myproject', format(model.config.get_project_name()))

            elif '// hls-fpga-machine-learning insert bram' in line:
                newline = line
                for bram in model_brams:
                    newline += f'#include \"firmware/weights/{bram.name}.h\"\n'

            elif '// hls-fpga-machine-learning insert header' in line:
                dtype = line.split('#', 1)[1].strip()
                if io_type == 'io_stream':
                    inputs_str = ', '.join([f'{dtype} {i.name}[{i.size_cpp()}]' for i in model_inputs])
                    outputs_str = ', '.join([f'{dtype} {o.name}[{o.size_cpp()}]' for o in model_outputs])
                else:
                    inputs_str = ', '.join([f'{dtype} {i.member_name}[{i.size_cpp()}]' for i in model_inputs])
                    outputs_str = ', '.join([f'{dtype} {o.member_name}[{o.size_cpp()}]' for o in model_outputs])

                insize_str = ', '.join([f'unsigned short &const_size_in_{i}' for i in range(1, len(model_inputs) + 1)])
                outsize_str = ', '.join([f'unsigned short &const_size_out_{o}' for o in range(1, len(model_outputs) + 1)])

                newline = ''
                newline += indent + inputs_str + ',\n'
                newline += indent + outputs_str + ',\n'
                newline += indent + insize_str + ',\n'
                newline += indent + outsize_str + '\n'

            elif '// hls-fpga-machine-learning insert wrapper' in line:
                bram_params = ''
                if model_brams:
                    bram_params = ', ' + ','.join([b.name for b in model_brams])

                dtype = line.split('#', 1)[1].strip()
                if io_type == 'io_stream':
                    newline = ''
                    for i in model_inputs:
                        # Initialise stream object and store input data (C-array) to a 'stream' object
                        newline += indent + f'stream_in<{i.type.name}> {i.name}_input;\n'
                        newline += indent + 'nnet::convert_data<{}, {}, {}>({}, {}_input);\n'.format(
                            dtype, i.type.name, i.size_cpp(), i.name, i.name
                        )

                    # Initialise stream output
                    for o in model_outputs:
                        newline += '\n'
                        newline += indent + f'stream_out<{o.type.name}> {o.name}_output;\n'

                    # Execute top-level function
                    input_params = ', '.join([f'{i.name}_input' for i in model_inputs])
                    output_params = ', '.join([f'{o.name}_output' for o in model_outputs])

                    top_level = (
                        indent + f'{model.config.get_project_name()}({input_params}, {output_params}{bram_params});\n'
                    )
                    newline += top_level
                    newline += '\n'

                    # Store data from 'stream' output to C-array, to be then returned and handled in Python
                    for o in model_outputs:
                        newline += indent + 'nnet::convert_data_back<{}, {}, {}>({}_output, {});\n'.format(
                            o.type.name, dtype, o.size_cpp(), o.name, o.name
                        )

                else:
                    # Convert input data from C-array to HLS type
                    newline = ''
                    newline += indent + 'input_data inputs_ap;\n'
                    for i in model_inputs:
                        newline += indent + 'nnet::convert_data<{}, {}, {}>({}, inputs_ap.{});\n'.format(
                            dtype, i.type.name, i.size_cpp(), i.member_name, i.member_name
                        )
                    newline += '\n'

                    # Initialise HLS output
                    newline += indent + 'output_data outputs_ap;\n'

                    # Execute top-level function
                    top_level = indent + f'outputs_ap = {model.config.get_project_name()}(inputs_ap{bram_params});\n'
                    newline += top_level
                    newline += '\n'

                    # Convert HLS outputs back to C-array
                    for o in model_outputs:
                        newline += indent + 'nnet::convert_data_back<{}, {}, {}>(outputs_ap.{}, {});\n'.format(
                            o.type.name, dtype, o.size_cpp(), o.member_name, o.member_name
                        )
            elif '// hls-fpga-machine-learning insert trace_outputs' in line:
                newline = ''
                for layer in model.get_layers():
                    func = layer.get_attr('function_cpp')
                    if func and model.config.trace_output and layer.get_attr('trace', False):
                        vars = layer.get_variables()
                        for var in vars:
                            newline += (
                                indent
                                + 'nnet::trace_outputs->insert(std::pair<std::string, void *>('
                                + f'"{layer.name}", (void *) malloc({var.size_cpp()} * element_size)));\n'
                            )

            else:
                newline = line
            fout.write(newline)

        f.close()
        fout.close()

    def write_build_script(self, model):
        """Write the build scripts (Makefile, build_lib.sh)

        Args:
            model (ModelGraph): the hls4ml model.
        """

        filedir = Path(__file__).parent

        # Makefile
        makefile_src = (filedir / '../templates/quartus/Makefile').resolve()
        makefile_dst = Path(f'{model.config.get_output_dir()}/Makefile').resolve()
        with open(makefile_src) as src, open(makefile_dst, 'w') as dst:
            for line in src.readlines():
                line = line.replace('myproject', model.config.get_project_name())

                if 'DEVICE   :=' in line:
                    line = 'DEVICE   := {}\n'.format(model.config.get_config_value('Part'))

                dst.write(line)

        # build_lib.sh
        build_lib_src = (filedir / '../templates/quartus/build_lib.sh').resolve()
        build_lib_dst = Path(f'{model.config.get_output_dir()}/build_lib.sh').resolve()
        with open(build_lib_src) as src, open(build_lib_dst, 'w') as dst:
            for line in src.readlines():
                line = line.replace('myproject', model.config.get_project_name())
                line = line.replace('mystamp', model.config.get_config_value('Stamp'))

                dst.write(line)
        build_lib_dst.chmod(build_lib_dst.stat().st_mode | stat.S_IEXEC)

    def write_nnet_utils(self, model):
        """Copy the nnet_utils, AP types headers and any custom source to the project output directory

        Args:
            model (ModelGraph): the hls4ml model.
        """

        # nnet_utils
        filedir = os.path.dirname(os.path.abspath(__file__))

        srcpath = os.path.join(filedir, '../templates/quartus/firmware/nnet_utils/')
        dstpath = f'{model.config.get_output_dir()}/firmware/nnet_utils/'

        if not os.path.exists(dstpath):
            os.mkdir(dstpath)

        headers = [os.path.basename(h) for h in glob.glob(srcpath + '*.h')]

        for h in headers:
            copyfile(srcpath + h, dstpath + h)

        # ac_types
        filedir = os.path.dirname(os.path.abspath(__file__))

        srcpath = os.path.join(filedir, '../templates/quartus/ac_types/')
        dstpath = f'{model.config.get_output_dir()}/firmware/ac_types/'

        if os.path.exists(dstpath):
            rmtree(dstpath)

        copytree(srcpath, dstpath)

        # custom source
        filedir = os.path.dirname(os.path.abspath(__file__))

        custom_source = get_backend('Quartus').get_custom_source()
        for dst, srcpath in custom_source.items():
            dstpath = f'{model.config.get_output_dir()}/firmware/{dst}'
            copyfile(srcpath, dstpath)

    def __get_table_size(self, model, activation):
        for layer in model.get_layers():
            if (
                layer.get_attr('activation') == activation or layer.get_attr('recurrent_activation') == activation
            ) and layer.get_attr('table_size') is not None:
                return int(layer.get_attr('table_size'))
        return 1024

    def __get_table_header(self, table_name, table_size):
        table_header = '#ifdef __INTELFPGA_COMPILER__\n'
        table_header += 'hls_init_on_powerup\n'
        table_header += '#endif\n'
        table_header += f'static const typename CONFIG_T::table_t {table_name}[{table_size}] = {{'
        return table_header

    def __write_elu_table(self, model, path):
        table_name = 'elu_table'
        table_size = self.__get_table_size(model, 'elu')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(table_size):
            in_val = -8.0 * i / float(table_size)
            real_val = np.exp(in_val) - 1.0
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_sigmoid_table(self, model, path):
        MAX_VALUE = 8
        MIN_VALUE = 0

        table_name = 'sigmoid_table'
        table_size = self.__get_table_size(model, 'sigmoid')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(int(table_size)):
            in_val = (
                i * (MAX_VALUE - MIN_VALUE) / float(table_size)
                + (MAX_VALUE - MIN_VALUE) / (float(table_size) * 2)
                + MIN_VALUE
            )
            real_val = 1.0 / (1 + np.exp(-in_val))
            if real_val >= 0.5:
                h_file.write(sep + str(real_val))
                sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_tanh_table(self, model, path):
        MAX_VALUE = 4
        MIN_VALUE = 0

        table_name = 'tanh_table'
        table_size = self.__get_table_size(model, 'tanh')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(table_size):
            in_val = (
                i * (MAX_VALUE - MIN_VALUE) / float(table_size)
                + (MAX_VALUE - MIN_VALUE) / (float(table_size) * 2)
                + MIN_VALUE
            )
            real_val = np.tanh(in_val)
            if real_val >= 0:
                h_file.write(sep + str(real_val))
                sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_softplus_table(self, model, path):
        table_name = 'softplus_table'
        table_size = self.__get_table_size(model, 'softplus')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(table_size):
            in_val = 2 * 8.0 * (i - float(table_size) / 2.0) / float(table_size)
            real_val = np.log(np.exp(in_val) + 1.0)
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_softsign_table(self, model, path):
        MAX_VALUE = 8
        MIN_VALUE = 0
        table_name = 'softsign_table'
        table_size = self.__get_table_size(model, 'softsign')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(table_size):
            in_val = (
                i * (MAX_VALUE - MIN_VALUE) / float(table_size)
                + (MAX_VALUE - MIN_VALUE) / (float(table_size) * 2)
                + MIN_VALUE
            )

            real_val = in_val / (np.fabs(in_val) + 1.0)
            if real_val >= 0:
                h_file.write(sep + str(real_val))
                sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_selu_table(self, model, path):
        table_name = 'selu_table'
        table_size = self.__get_table_size(model, 'selu')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(table_size):
            in_val = -8.0 * i / float(table_size)
            real_val = 1.0507009873554804934193349852946 * (1.6732632423543772848170429916717 * (np.exp(in_val) - 1.0))
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_exp_table(self, model, path):
        table_name = 'exp_table'
        table_size = self.__get_table_size(model, 'softmax')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        # Default fixed point precision
        # 6 bits for integer part, 10 bits for decimal - total, 16
        fp_bits = 16
        fp_integer = 6
        fp_signed = True

        # Exp table should use the same precision as exp_table, as seen in Vivado code
        # init_exp_table<data_T, CONFIG_T>(exp_table);
        for layer in model.get_layers():
            if layer.name == 'softmax':
                ac_type = layer.get_input_variable().type
                if ac_type is not None:
                    try:
                        fp_bits = ac_type.precision.integer + ac_type.precision.fractional
                        fp_integer = ac_type.precision.integer
                        fp_signed = ac_type.precision.signed
                    except Exception:
                        # FixedPrecisionType wasn't correctly stored in layer attributes, use default values
                        pass
                    if fp_signed is False:
                        raise Exception('Softmax types need to be signed')

        sep = ''
        N = ceil_log2(table_size)
        for i in range(table_size):
            f = FixedPointEmulator(fp_bits, fp_integer, signed=fp_signed)
            b = uint_to_binary(i, N)
            if i == 0:
                b.insert(0, 0)
            else:
                b.insert(0, 1)
            f.set_msb_bits(b)
            real_val = f.exp_float()
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_invert_table(self, model, path):
        table_name = 'invert_table'
        table_size = self.__get_table_size(model, 'softmax')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        # Default fixed point precision, in case values from layer attributes cannot be extracted
        # 8 bits for integer part, 10 bits for decimal - total, 18
        fp_bits = 18
        fp_integer = 8
        fp_signed = True

        # Invert table should use the same precision as exp_table, as seen in Vivado code
        # init_invert_table<typename CONFIG_T::exp_table_t, CONFIG_T>(invert_table);
        for layer in model.get_layers():
            if layer.name == 'softmax':
                ac_type = layer.get_attr('exp_table_t')
                if ac_type is not None:
                    try:
                        fp_bits = ac_type.precision.integer + ac_type.precision.fractional
                        fp_integer = ac_type.precision.integer
                        fp_signed = ac_type.precision.signed
                    except Exception:
                        # FixedPrecisionType wasn't correctly stored in layer attributes, use default values
                        pass
                    if fp_signed is False:
                        raise Exception('Softmax types need to be signed')

        sep = ''
        N = ceil_log2(table_size)
        for i in range(table_size):
            f = FixedPointEmulator(fp_bits, fp_integer, signed=fp_signed)
            b = uint_to_binary(i, N)
            b.insert(0, 0)
            f.set_msb_bits(b)
            real_val = f.inv_float()
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_exp_table_latency(self, model, path):
        table_name = 'exp_table_latency'
        table_size = self.__get_table_size(model, 'softmax')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        # Default fixed point precision
        # 6 bits for integer part, 10 bits for decimal - total, 16
        fp_bits = 16
        fp_integer = 6
        fp_signed = True

        # Exp table should use the same precision as exp_table, as seen in Vivado code
        # init_exp_table<data_T, CONFIG_T>(exp_table);
        for layer in model.get_layers():
            if layer.name == 'softmax':
                ac_type = layer.get_input_variable().type
                if ac_type is not None:
                    try:
                        fp_bits = ac_type.precision.integer + ac_type.precision.fractional
                        fp_integer = ac_type.precision.integer
                        fp_signed = ac_type.precision.signed
                    except Exception:
                        # FixedPrecisionType wasn't correctly stored in layer attributes, use default values
                        pass

        sep = ''
        N = ceil_log2(table_size)
        for i in range(table_size):
            f = FixedPointEmulator(fp_bits, fp_integer, signed=fp_signed)
            f.set_msb_bits(uint_to_binary(i, N))
            real_val = f.exp_float()
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_invert_table_latency(self, model, path):
        table_name = 'invert_table_latency'
        table_size = self.__get_table_size(model, 'softmax')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        # Default fixed point precision, in case values from layer attributes cannot be extracted
        # 8 bits for integer part, 10 bits for decimal - total, 18
        fp_bits = 18
        fp_integer = 8
        fp_signed = True

        # Invert table should use the same precision as exp_table, as seen in Vivado code
        # init_invert_table<typename CONFIG_T::exp_table_t, CONFIG_T>(invert_table);
        for layer in model.get_layers():
            if layer.name == 'softmax':
                ac_type = layer.get_attr('exp_table_t')
                if ac_type is not None:
                    try:
                        fp_bits = ac_type.precision.integer + ac_type.precision.fractional
                        fp_integer = ac_type.precision.integer
                        fp_signed = ac_type.precision.signed
                    except Exception:
                        # FixedPrecisionType wasn't correctly stored in layer attributes, use default values
                        pass

        sep = ''
        N = ceil_log2(table_size)
        for i in range(table_size):
            f = FixedPointEmulator(fp_bits, fp_integer, signed=fp_signed)
            f.set_msb_bits(uint_to_binary(i, N))
            real_val = f.inv_float()
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_exp_table_legacy(self, model, path):
        table_name = 'exp_table_legacy'
        table_size = self.__get_table_size(model, 'softmax')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(table_size):
            in_val = 2 * 8.0 * (i - float(table_size) / 2.0) / float(table_size)
            real_val = np.exp(in_val)
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def __write_invert_table_legacy(self, model, path):
        table_name = 'invert_table_legacy'
        table_size = self.__get_table_size(model, 'softmax')

        h_file = open(f'{path}/{table_name}.tb', 'w')
        h_file.write(self.__get_table_header(table_name, table_size))

        sep = ''
        for i in range(table_size):
            real_val = 0
            in_val = 64.0 * i / float(table_size)
            if in_val > 0.0:
                real_val = 1.0 / in_val
            h_file.write(sep + str(real_val))
            sep = ", "

        h_file.write('};\n')
        h_file.close()

    def write_activation_tables(self, model):
        """Write the lookup tables for activation functions

        Args:
            model (ModelGraph): the hls4ml model.
        """
        # Output path
        dstpath = f'{model.config.get_output_dir()}/firmware/nnet_utils/activation_tables'
        if not os.path.exists(dstpath):
            os.mkdir(dstpath)

        # Tables
        # TODO - Only write tables needed by model, not all of them
        self.__write_elu_table(model, dstpath)
        self.__write_sigmoid_table(model, dstpath)
        self.__write_tanh_table(model, dstpath)
        self.__write_softplus_table(model, dstpath)
        self.__write_softsign_table(model, dstpath)
        self.__write_selu_table(model, dstpath)
        self.__write_exp_table(model, dstpath)
        self.__write_invert_table(model, dstpath)
        self.__write_exp_table_latency(model, dstpath)
        self.__write_invert_table_latency(model, dstpath)
        self.__write_exp_table_legacy(model, dstpath)
        self.__write_invert_table_legacy(model, dstpath)

    def write_yml(self, model):
        """Write the config to the YAML file

        Args:
            model (ModelGraph): the hls4ml model.
        """

        def keras_model_representer(dumper, keras_model):
            model_path = model.config.get_output_dir() + '/keras_model.keras'
            keras_model.save(model_path)
            return dumper.represent_scalar('!keras_model', model_path)

        try:
            import keras

            KerasModel = keras.models.Model

            yaml.add_multi_representer(KerasModel, keras_model_representer)
        except Exception:
            pass

        with open(model.config.get_output_dir() + '/' + config_filename, 'w') as file:
            yaml.dump(model.config.config, file)

    def write_tar(self, model):
        """Write the generated project as a .tar.gz archive

        Args:
            model (ModelGraph): the hls4ml model.
        """

        if model.config.get_writer_config().get('WriteTar', False):
            tar_path = model.config.get_output_dir() + '.tar.gz'
            if os.path.exists(tar_path):
                os.remove(tar_path)
            with tarfile.open(model.config.get_output_dir() + '.tar.gz', mode='w:gz') as archive:
                archive.add(model.config.get_output_dir(), recursive=True)

    def write_hls(self, model):
        print('Writing HLS project')
        self.write_project_dir(model)
        self.write_project_cpp(model)
        self.write_project_header(model)
        self.write_weights(model)
        self.write_defines(model)
        self.write_parameters(model)
        self.write_test_bench(model)
        self.write_bridge(model)
        self.write_build_script(model)
        self.write_nnet_utils(model)
        self.write_activation_tables(model)
        self.write_yml(model)
        self.write_tar(model)
        print('Done')
